define(["require", "exports", 'requires-port', 'querystringify'], function (require, exports, required, qs) {
	'use strict';

	var protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i
		, slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//;

	/**
	 * These are the parse rules for the URL parser, it informs the parser
	 * about:
	 *
	 * 0. The char it Needs to parse, if it's a string it should be done using
	 *    indexOf, RegExp using exec and NaN means set as current value.
	 * 1. The property we should set when parsing this value.
	 * 2. Indication if it's backwards or forward parsing, when set as number it's
	 *    the value of extra chars that should be split off.
	 * 3. Inherit from location if non existing in the parser.
	 * 4. `toLowerCase` the resulting value.
	 */
	var rules = [
		['#', 'hash'],                        // Extract from the back.
		['?', 'query'],                       // Extract from the back.
		['/', 'pathname'],                    // Extract from the back.
		['@', 'auth', 1],                     // Extract from the front.
		[NaN, 'host', undefined, 1, 1],       // Set left over value.
		[/:(\d+)$/, 'port', undefined, 1],    // RegExp the back.
		[NaN, 'hostname', undefined, 1, 1]    // Set left over.
	];

	/**
	 * These properties should not be copied or inherited from. This is only needed
	 * for all non blob URL's as a blob URL does not include a hash, only the
	 * origin.
	 *
	 * @type {Object}
	 * @private
	 */
	var ignore = { hash: 1, query: 1 };

	/**
	 * The location object differs when your code is loaded through a normal page,
	 * Worker or through a worker using a blob. And with the blobble begins the
	 * trouble as the location object will contain the URL of the blob, not the
	 * location of the page where our code is loaded in. The actual origin is
	 * encoded in the `pathname` so we can thankfully generate a good "default"
	 * location from it so we can generate proper relative URL's again.
	 *
	 * @param {Object|String} loc Optional default location object.
	 * @returns {Object} lolcation object.
	 * @api public
	 */
	function lolcation(loc) {
		loc = loc || global.location || {};

		var finaldestination = {}
			, type = typeof loc
			, key;

		if ('blob:' === loc.protocol) {
			finaldestination = new URL(unescape(loc.pathname), {});
		} else if ('string' === type) {
			finaldestination = new URL(loc, {});
			for (key in ignore) delete finaldestination[key];
		} else if ('object' === type) {
			for (key in loc) {
				if (key in ignore) continue;
				finaldestination[key] = loc[key];
			}

			if (finaldestination.slashes === undefined) {
				finaldestination.slashes = slashes.test(loc.href);
			}
		}

		return finaldestination;
	}

	/**
	 * @typedef ProtocolExtract
	 * @type Object
	 * @property {String} protocol Protocol matched in the URL, in lowercase.
	 * @property {Boolean} slashes `true` if protocol is followed by "//", else `false`.
	 * @property {String} rest Rest of the URL that is not part of the protocol.
	 */

	/**
	 * Extract protocol information from a URL with/without double slash ("//").
	 *
	 * @param {String} address URL we want to extract from.
	 * @return {ProtocolExtract} Extracted information.
	 * @api private
	 */
	function extractProtocol(address) {
		var match = protocolre.exec(address);

		return {
			protocol: match[1] ? match[1].toLowerCase() : '',
			slashes: !!match[2],
			rest: match[3]
		};
	}

	/**
	 * Resolve a relative URL pathname against a base URL pathname.
	 *
	 * @param {String} relative Pathname of the relative URL.
	 * @param {String} base Pathname of the base URL.
	 * @return {String} Resolved pathname.
	 * @api private
	 */
	function resolve(relative, base) {
		var path = (base || '/').split('/').slice(0, -1).concat(relative.split('/'))
			, i = path.length
			, last = path[i - 1]
			, unshift = false
			, up = 0;

		while (i--) {
			if (path[i] === '.') {
				path.splice(i, 1);
			} else if (path[i] === '..') {
				path.splice(i, 1);
				up++;
			} else if (up) {
				if (i === 0) unshift = true;
				path.splice(i, 1);
				up--;
			}
		}

		if (unshift) path.unshift('');
		if (last === '.' || last === '..') path.push('');

		return path.join('/');
	}

	/**
	 * The actual URL instance. Instead of returning an object we've opted-in to
	 * create an actual constructor as it's much more memory efficient and
	 * faster and it pleases my OCD.
	 *
	 * @constructor
	 * @param {String} address URL we want to parse.
	 * @param {Object|String} location Location defaults for relative paths.
	 * @param {Boolean|Function} parser Parser for the query string.
	 * @api public
	 */
	function URL(address, location, parser) {
		if (!(this instanceof URL)) {
			return new URL(address, location, parser);
		}

		var relative, extracted, parse, instruction, index, key
			, instructions = rules.slice()
			, type = typeof location
			, url = this
			, i = 0;

		//
		// The following if statements allows this module two have compatibility with
		// 2 different API:
		//
		// 1. Node.js's `url.parse` api which accepts a URL, boolean as arguments
		//    where the boolean indicates that the query string should also be parsed.
		//
		// 2. The `URL` interface of the browser which accepts a URL, object as
		//    arguments. The supplied object will be used as default values / fall-back
		//    for relative paths.
		//
		if ('object' !== type && 'string' !== type) {
			parser = location;
			location = null;
		}

		if (parser && 'function' !== typeof parser) parser = qs.parse;

		location = lolcation(location);

		//
		// Extract protocol information before running the instructions.
		//
		extracted = extractProtocol(address || '');
		relative = !extracted.protocol && !extracted.slashes;
		url.slashes = extracted.slashes || relative && location.slashes;
		url.protocol = extracted.protocol || location.protocol || '';
		address = extracted.rest;

		//
		// When the authority component is absent the URL starts with a path
		// component.
		//
		if (!extracted.slashes) instructions[2] = [/(.*)/, 'pathname'];

		for (; i < instructions.length; i++) {
			instruction = instructions[i];
			parse = instruction[0];
			key = instruction[1];

			if (parse !== parse) {
				url[key] = address;
			} else if ('string' === typeof parse) {
				if (~(index = address.indexOf(parse))) {
					if ('number' === typeof instruction[2]) {
						url[key] = address.slice(0, index);
						address = address.slice(index + instruction[2]);
					} else {
						url[key] = address.slice(index);
						address = address.slice(0, index);
					}
				}
			} else if ((index = parse.exec(address))) {
				url[key] = index[1];
				address = address.slice(0, index.index);
			}

			url[key] = url[key] || (
				relative && instruction[3] ? location[key] || '' : ''
			);

			//
			// Hostname, host and protocol should be lowercased so they can be used to
			// create a proper `origin`.
			//
			if (instruction[4]) url[key] = url[key].toLowerCase();
		}

		//
		// Also parse the supplied query string in to an object. If we're supplied
		// with a custom parser as function use that instead of the default build-in
		// parser.
		//
		if (parser) url.query = parser(url.query);

		//
		// If the URL is relative, resolve the pathname against the base URL.
		//
		if (
			relative
			&& location.slashes
			&& url.pathname.charAt(0) !== '/'
			&& (url.pathname !== '' || location.pathname !== '')
		) {
			url.pathname = resolve(url.pathname, location.pathname);
		}

		//
		// We should not add port numbers if they are already the default port number
		// for a given protocol. As the host also contains the port number we're going
		// override it with the hostname which contains no port number.
		//
		if (!required(url.port, url.protocol)) {
			url.host = url.hostname;
			url.port = '';
		}

		//
		// Parse down the `auth` for the username and password.
		//
		url.username = url.password = '';
		if (url.auth) {
			instruction = url.auth.split(':');
			url.username = instruction[0] || '';
			url.password = instruction[1] || '';
		}

		url.origin = url.protocol && url.host && url.protocol !== 'file:'
			? url.protocol + '//' + url.host
			: 'null';

		//
		// The href is just the compiled result.
		//
		url.href = url.toString();
	}

	/**
	 * This is convenience method for changing properties in the URL instance to
	 * insure that they all propagate correctly.
	 *
	 * @param {String} part          Property we need to adjust.
	 * @param {Mixed} value          The newly assigned value.
	 * @param {Boolean|Function} fn  When setting the query, it will be the function
	 *                               used to parse the query.
	 *                               When setting the protocol, double slash will be
	 *                               removed from the final url if it is true.
	 * @returns {URL}
	 * @api public
	 */
	function set(part, value, fn) {
		var url = this;

		switch (part) {
			case 'query':
				if ('string' === typeof value && value.length) {
					value = (fn || qs.parse)(value);
				}

				url[part] = value;
				break;

			case 'port':
				url[part] = value;

				if (!required(value, url.protocol)) {
					url.host = url.hostname;
					url[part] = '';
				} else if (value) {
					url.host = url.hostname + ':' + value;
				}

				break;

			case 'hostname':
				url[part] = value;

				if (url.port) value += ':' + url.port;
				url.host = value;
				break;

			case 'host':
				url[part] = value;

				if (/:\d+$/.test(value)) {
					value = value.split(':');
					url.port = value.pop();
					url.hostname = value.join(':');
				} else {
					url.hostname = value;
					url.port = '';
				}

				break;

			case 'protocol':
				url.protocol = value.toLowerCase();
				url.slashes = !fn;
				break;

			case 'pathname':
			case 'hash':
				if (value) {
					var char = part === 'pathname' ? '/' : '#';
					url[part] = value.charAt(0) !== char ? char + value : value;
				} else {
					url[part] = value;
				}
				break;

			default:
				url[part] = value;
		}

		for (var i = 0; i < rules.length; i++) {
			var ins = rules[i];

			if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase();
		}

		url.origin = url.protocol && url.host && url.protocol !== 'file:'
			? url.protocol + '//' + url.host
			: 'null';

		url.href = url.toString();

		return url;
	}

	/**
	 * Transform the properties back in to a valid and full URL string.
	 *
	 * @param {Function} stringify Optional query stringify function.
	 * @returns {String}
	 * @api public
	 */
	function toString(stringify) {
		if (!stringify || 'function' !== typeof stringify) stringify = qs.stringify;

		var query
			, url = this
			, protocol = url.protocol;

		if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':';

		var result = protocol + (url.slashes ? '//' : '');

		if (url.username) {
			result += url.username;
			if (url.password) result += ':' + url.password;
			result += '@';
		}

		result += url.host + url.pathname;

		query = 'object' === typeof url.query ? stringify(url.query) : url.query;
		if (query) result += '?' !== query.charAt(0) ? '?' + query : query;

		if (url.hash) result += url.hash;

		return result;
	}

	URL.prototype = { set: set, toString: toString };

	//
	// Expose the URL parser and some additional properties that might be useful for
	// others or testing.
	//
	URL.extractProtocol = extractProtocol;
	URL.location = lolcation;
	URL.qs = qs;

	exports.resolve = resolve;
});